//FAS低代码开发平台
//变更日志插件: 针对CUD操作记录变更日志，其中新增、修改的克隆TRecord逻辑是避免状态机插件【保存后事件】影响返给前端的TRecord内容
//新增：
//    1、前端NewTRecord A传入后【BeforeExecuteSQL】做初次变更比较（没有OldValue），为A生成ChangeSet供状态机【保存前事件】Action脚本使用
//    2、数据落盘后【Finish】，从DB中取出最新NewTRecord B（获取DB默认值），克隆NewTRecord C，将B传入插件，再次做变更比较。
//       此时生成的变更为包含状态机脚本更新后的最新变更，直接生成大数据日志，而后执行状态机的【保存后事件】
//    3、将C返回给前端。
//修改：
//    1、前端NewTRecord A传入后【BeforeExecuteSQL】，获取DB中的OldRecord，做变更比较，将NewTRecord A,OldValue传入供状态机【保存前事件】Action脚本使用
//    2、状态机【保存前事件】执行完毕后（A的ChangeSet已包含最新变更），克隆NewTRecord B，
//       数据落盘后【Finish】，将A传入变更日志插件中，直接生成大数据日志。而后执行状态机的【保存后事件】
//    3、将最新的时间戳赋值给B后返回前端。
//删除：
//    1、由于删除不走状态机逻辑，故在【BeforeExecuteSQL】时，变更日志插件中生成TRecord实例，只记录主键id变更，放到上下文中
//    2、数据落盘后【Finish】，在上下文中拿到生成的TRecord实例，生成大数据日志
//    3、删除不向前端返回数据
//===============================================================================
//变更历史纪录
//日期			版本	修改人	描述
//===============================================================================

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using T.FAS.Runtime.Base.Entity;
using T.FAS.Runtime.DataAccess.Common;
using T.FAS.Runtime.DataAccess.SPI;
using T.FAS.Runtime.DataAccess.SQLDom;
using T.FAS.Runtime.Metadata.Entity.DataModel;
using T.ServiceComponent.Log.SPI;

namespace T.FAS.Runtime.DataAccess.Engine
{
    /// <summary>
    /// 变更日志基类
    /// </summary>
    public abstract class AbstractChangeLog : IChangeLog
    {
        private static readonly IDACLogService s_localLogService = DACLogFactory.GetDACLogService();
        protected TChangeLog ChangeSetTemplate { get; set; }
        protected TChangeBasicInfo BasicChangeInfo { get; set; }

        public abstract TRecord LogChangeSet(ChangeLogContext changeLogContext);
        public abstract DataChangeLog LogBigDataChangeLog(ChangeLogContext changeLogContext);
        public abstract TChangeLog LogChange(ChangeLogContext changeLogContext);

        #region 创建依托以TRecord的ChangeSet
        /// <summary>
        /// 递归构造新增Record的ChangeSet变更项
        /// </summary>
        /// <param name="cno"></param>
        /// <param name="newRecords"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        protected void GetCreateChangeSetRecursive(CompositeNodeObject cno, List<TRecord> newRecords, ChangeLogControlContext controlContext ,int depth = 0)
        {
            foreach (var record in newRecords)
            {   
                InitTRecordChangeSet(cno, record, controlContext, depth);
            }
            if (cno.ChildCompositeNodeObjects != null)
            {
                foreach (var childCNO in cno.ChildCompositeNodeObjects)
                {
                    List<TRecord> childRecords = new List<TRecord>();
                    //根据子节点CNO与当前节点的关联类型，在TRecord中取出子节点集合
                    var relationtype = childCNO.GetRelationType();
                    foreach (var record in newRecords)
                    {
                        var childData = record[childCNO.GetCNODataObjectTableAlias()];
                        if (childData != null)
                        {
                            if(relationtype == 0)
                            {
                                if (childData is List<TRecord> cRecords)
                                {
                                    childRecords.AddRange(cRecords);
                                }
                            }
                            else
                            {
                                if (childData is TRecord cRecord)
                                {
                                    childRecords.Add(cRecord);
                                }
                            }
                        }
                    }
                    if (childRecords.Count > 0)
                    {
                        GetCreateChangeSetRecursive(childCNO, childRecords, controlContext, depth + 1);
                    }
                }
            }
        }

        /// <summary>
        /// Create变更初始化，获取当前节点的TRecord的变更，要求TRecord的主键列必须赋值
        /// </summary>
        /// <param name="cno"></param>
        /// <param name="newRecord"></param>
        /// <returns></returns>
        protected void InitTRecordChangeSet(CompositeNodeObject cno, TRecord newRecord, ChangeLogControlContext controlContext, int depth = 0)
        {
            var normalEle = cno.Elements.Where(e => e.ElementType == ElementType.Normal).ToList();
            string innerAssoEleName = null;
            var pkColName = cno.GetPKColumn().ColumnName;
            if (!cno.IsRootObject)
            {
                var innerAssoEle = cno.GetInnerAssociateElement();
                innerAssoEleName = innerAssoEle.Alias ?? innerAssoEle.Name;
            }
            var billID = newRecord.GetValue(pkColName)?.ToString();
            newRecord.ChangeType = TRecordChangeType.Add;
            newRecord.ChangeSet = new TChangeSet
            {
                ChangeType = TRecordChangeType.Add,
                BillID = billID,
                CDOID = cno.CompositeDataObjectID,
                CDOCode = cno.CompositeDataObject?.Code,
                PKColumnName = pkColName,
                PKColumnValue = billID,
                TenantID = BasicChangeInfo.TenantID,
                Modifier = BasicChangeInfo.Modifier,
                ModifierName = BasicChangeInfo.ModifierName,
                ModifyTime = BasicChangeInfo.ModifyTime,
            };

            var dataChangeSetItems = new List<TChangeSetItem>();
            //初始化变更项
            foreach (var ele in normalEle)
            {
                DataColumn col = ele.GetDataColumn(cno);
                if (col == null)
                    throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_InitTRecordChangeSet,cno.CompositeDataObjectID,cno.DataObjectTableAlias,ele.Name,ele.DataColumnID));
                /***************数据国际化改造：ChangeSet Create比较支持多语言字段****************/
                if (col.IsInternational)
                {
                    Dictionary<string, string> mapping = InternationalTools.GetLanguageMapping(controlContext.InternationalMode, controlContext.SpecifiedLanguages);
                    if (mapping.Count > 0)
                    {
                        foreach (string i18NCode in mapping.Values)
                        {
                            string tRecordPropName = InternationalTools.GetLogicFieldName(col.ColumnName, i18NCode);
                            if (newRecord.IsContainProperty(tRecordPropName))
                            {
                                //新增时DBNull类型的不用记录
                                var item = ConstructCrateChangeSetItem(ele, newRecord, tRecordPropName, col.DisplayName + i18NCode);
                                if(item != null) dataChangeSetItems.Add(item);
                            }
                        }
                    }
                }
                //统一数据访问服务掩码改造，掩码字段新增时不需要记录_mask掩码值
                else
                {
                    if (newRecord.IsContainProperty(col.ColumnName))
                    {
                        //新增时DBNull类型的不用记录
                        var item = ConstructCrateChangeSetItem(ele, newRecord, ele.Alias ?? ele.Name, col.DisplayName);
                        if (item != null) dataChangeSetItems.Add(item);
                    }
                }
            }
            newRecord.ChangeSet.AddRangeChangeData(dataChangeSetItems);
        }

        /// <summary>
        /// 获取删除记录的变更项
        /// </summary>
        /// <param name="changeLogContext"></param>
        /// <returns></returns>
        protected TRecord GetDeleteChangeSet(CompositeDataObject cdo, CompositeNodeObject cno, List<object> PKVal)
        {
            var ele = cno.GetPKColumn();
            TRecord record = new TRecord();
            record.ChangeType = TRecordChangeType.Delete;
            record.PKColumnName = ele?.ColumnName;
            record.PKColumnValue = PKVal.FirstOrDefault()?.ToString();
            record.ChangeSet = new TChangeSet
            {
                BillID = record.PKColumnValue,
                CDOID = cdo?.ID,
                CDOCode = cdo?.Code,
                ChangeType = TRecordChangeType.Delete,
                PKColumnName = ele?.ColumnName,
                PKColumnValue = record.PKColumnValue,
                ModifyTime = DateTime.Now,
                Modifier = UserUtil.GetCurrentUserID(),
                ModifierName = UserUtil.GetCurrentUserName(),
                TenantID = UserUtil.GetCurrentUserTenantId(),
            };
            return record;
        }
        #endregion

        #region 创建大数据变更日志
        public DataChangeLog CreateNewBigDataChangeLog(ChangeLogContext changeLogContext, bool isDel = false)
        {
            var pkCol = changeLogContext.CompositeDataObject.RootCompositeNodeObject.GetPKColumn();
            DataChangeLog dataChangeLog = new DataChangeLog();
            dataChangeLog.BillType = changeLogContext.CompositeDataObject.Code;
            dataChangeLog.BillName = changeLogContext.CompositeDataObject.Name;
            dataChangeLog.BillID = isDel ? changeLogContext.CurrentPK.ToString() : changeLogContext.CurrentData.GetValue(pkCol.ColumnName).ToString();
            dataChangeLog.Action = changeLogContext.InjectionMethodType.ToString();
            dataChangeLog.UserID = UserUtil.GetCurrentUserID();

            dataChangeLog.Ext15 = changeLogContext.CompositeDataObject.ID;
            dataChangeLog.Ext16 = "T";
            dataChangeLog.Ext17 = changeLogContext.CompositeDataObject.Code;
            dataChangeLog.Ext18 = changeLogContext.CompositeDataObject.Name;
            dataChangeLog.Ext19 = UserUtil.GetCurrentUserName();

            return dataChangeLog;
        }

        /// <summary>
        /// 递归获取新增Record的变更项
        /// </summary>
        /// <param name="cno"></param>
        /// <param name="newRecords"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        protected List<DataChangeItem> GetBigDataCreateRecordChangeRecursive(CompositeNodeObject cno, List<TRecord> newRecords, int depth = 0)
        {
            List<DataChangeItem> dataChangeItems = new List<DataChangeItem>();
            if (depth > 1)  //三级及以下的明细不记录变更项，在二级json中记录
                return dataChangeItems;
            foreach (var record in newRecords)
            {
                dataChangeItems.AddRange(GetBigDataCreateRecordChange(cno, record, depth));
            }
            if (cno.ChildCompositeNodeObjects != null)
            {
                foreach (var childCNO in cno.ChildCompositeNodeObjects)
                {
                    List<TRecord> childRecords = new List<TRecord>();
                    foreach (var record in newRecords)
                    {
                        var cRecords = (List<TRecord>)record[childCNO.GetCNODataObjectTableAlias()];
                        if (cRecords != null)
                        {
                            childRecords.AddRange(cRecords);
                        }
                    }
                    if (childRecords.Count > 0)
                    {
                        dataChangeItems.AddRange(GetBigDataCreateRecordChangeRecursive(childCNO, childRecords, depth + 1));
                    }
                }
            }
            return dataChangeItems;
        }

        /// <summary>
        /// 获取当前节点的TRecord的变更，要求TRecord的主键列必须赋值
        /// </summary>
        /// <param name="cno"></param>
        /// <param name="newRecord"></param>
        /// <returns></returns>
        protected List<DataChangeItem> GetBigDataCreateRecordChange(CompositeNodeObject cno, TRecord newRecord, int depth = 0)
        {
            var normalEle = cno.Elements.Where(e => e.ElementType == ElementType.Normal).ToList();
            var dataChangeItems = new List<DataChangeItem>();
            string innerAssoEleName = null;
            var pkColName = cno.GetPKColumn().ColumnName;
            if (!cno.IsRootObject)
            {
                var innerAssoEle = cno.GetInnerAssociateElement();
                innerAssoEleName = innerAssoEle.Alias ?? innerAssoEle.Name;
            }
            //depth=0的时候记录字段变更明细，否则记录TRecord大json
            if (depth == 0)
            {
                foreach (var ele in normalEle)
                {
                    var col = ele.GetDataColumn(cno);
                    if (newRecord.IsContainProperty(col.ColumnName))
                    {
                        object newVal = newRecord.GetValue(col.ColumnName);
                        var id = newRecord.GetValue(pkColName)?.ToString();
                        var sid = innerAssoEleName == null
                                            ? newRecord.GetValue(pkColName)?.ToString()
                                            : newRecord.GetValue(innerAssoEleName)?.ToString();
                        dataChangeItems.Add(new DataChangeItem
                        {
                            ID = id,
                            SID = sid,
                            DataColumn = col.DisplayName,
                            NewData = newVal.ToString(),    //将变更字段都转为string可能会出问题
                            OldData = null
                        });
                    }
                }
            }
            else
            {
                var id = newRecord.GetValue(pkColName)?.ToString();
                var sid = innerAssoEleName == null
                                    ? newRecord.GetValue(pkColName)?.ToString()
                                    : newRecord.GetValue(innerAssoEleName)?.ToString();
                dataChangeItems.Add(new DataChangeItem
                {
                    ID = id,
                    SID = sid,
                    DataColumn = cno.DataObject.Name,
                    NewData = JsonSerialize(newRecord),     //子级及更下级的节点记录序列化json
                    OldData = null,
                });
            }
            return dataChangeItems;
        }

        /// <summary>
        /// 获取删除记录的变更项
        /// </summary>
        /// <param name="changeLogContext"></param>
        /// <returns></returns>
        protected List<DataChangeItem> GetBigDataDeleteRecordChange(CompositeNodeObject cno, List<object> PKVal)
        {
            if (PKVal == null)
                return null;
            List<DataChangeItem> dataChangeItems = new List<DataChangeItem>();
            foreach (var pk in PKVal)
            {
                dataChangeItems.Add(
                new DataChangeItem
                {
                    ID = pk.ToString(),
                    SID = cno.DataObjectTableAlias,
                });
            }
            return dataChangeItems;
        }

        /// <summary>
        /// 比较CNO节点下两个Record是否有变更
        /// </summary>
        /// <param name="cno">CNO节点元数据</param>
        /// <param name="newRecord">新Record</param>
        /// <param name="oldRecord">旧Record</param>
        /// <param name="dataChangeItems">返回的变更项</param>
        /// <returns>True:已变更,False:无变更</returns>
        protected bool Compare(CompositeNodeObject cno, TRecord newRecord, TRecord oldRecord, out List<DataChangeItem> dataChangeItems, int depth = 0)
        {
            var pkEle = cno.GetPKElement();
            var pkColName = cno.GetPKColumn().ColumnName;
            if (newRecord == null)
                throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_NewRecordIsNull));
            if (!FieldCompare(newRecord, oldRecord, pkEle))
                throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_RecordCompareFail_PkValueNotEqual));

            string innerAssoEleName = null;
            if (!cno.IsRootObject)
            {
                var innerAssoEle = cno.GetInnerAssociateElement();
                innerAssoEleName = innerAssoEle.Alias ?? innerAssoEle.Name;
            }

            dataChangeItems = new List<DataChangeItem>();
            var detailChangeItems = new List<DataChangeItem>(); //明细变更项，只有主节点使用
            bool IsRecordChanged = false;
            var normalEle = cno.Elements.Where(e => e.ElementType == ElementType.Normal).ToList();
            foreach (var ele in normalEle)
            {
                var col = ele.GetDataColumn(cno);
                if (newRecord.IsContainProperty(col.ColumnName))
                {
                    object oldVal = oldRecord.GetValueForDAC(col.ColumnName);
                    object newVal = newRecord.GetValueForDAC(col.ColumnName);
                    oldVal = PreprocessField(oldVal, ele.DataType);
                    newVal = PreprocessField(newVal, ele.DataType);
                    var flag = FieldCompare(oldVal, newVal, ele.DataType);
                    //判断字段是否变更，Record.Value为数据库取出来的类型，被DT做了类型转换，暂时先用默认的Equals方法
                    if (!flag)  //只记录变更项
                    {
                        IsRecordChanged = true;
                        detailChangeItems.Add(new DataChangeItem
                        {
                            ID = col.ColumnName,
                            SID = col.ColumnName,
                            DataColumn = col.DisplayName,
                            NewData = newVal.ToString(),    //将变更字段都转为string可能会出问题
                            OldData = oldVal.ToString(),    //
                        });
                    }
                }
            }
            if (IsRecordChanged)
            {
                if (depth == 0)
                {
                    dataChangeItems = detailChangeItems;
                }
                else
                {
                    dataChangeItems.Add(new DataChangeItem
                    {
                        ID = newRecord.GetValue(pkColName)?.ToString(),
                        SID = innerAssoEleName == null
                                            ? newRecord.GetValue(pkColName)?.ToString()
                                            : newRecord.GetValue(innerAssoEleName)?.ToString(),
                        DataColumn = cno.DataObject.Name,
                        NewData = JsonSerialize(newRecord),     //子级及更下级的节点记录序列化json
                        OldData = JsonSerialize(oldRecord),
                    });
                }
            }
            return IsRecordChanged;
        }
        #endregion

        #region 创建通用变更日志
        /// <summary>
        /// 创建标准日志
        /// </summary>
        /// <param name="changeLogContext"></param>
        /// <param name="isDel"></param>
        /// <returns></returns>
        public TChangeLog CreateDataChangeSet(ChangeLogContext changeLogContext, bool isDel = false)
        {
            if (changeLogContext?.CompositeDataObject == null)
                throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_CreateDataChangeSet));
            var pkCol = changeLogContext.CompositeDataObject.RootCompositeNodeObject.GetPKColumn();
            TChangeLog changeSet = new TChangeLog(MappingInjectionTypeToChangeType(changeLogContext.InjectionMethodType)
                                        , UserUtil.GetCurrentUserID()
                                        , DateTime.Now);
            changeSet.CDOCode   = changeLogContext.CompositeDataObject.Code;
            changeSet.CDOName   = changeLogContext.CompositeDataObject.Name;
            changeSet.BillID    = isDel ? changeLogContext.CurrentPK.ToString() : changeLogContext.CurrentData?.GetValue(pkCol.ColumnName)?.ToString();
            changeSet.CDOID     = changeLogContext.CompositeDataObject.ID;
            changeSet.TenantID  = "T";
            changeSet.CDOCode   = changeLogContext.CompositeDataObject.Code;
            changeSet.CDOName   = changeLogContext.CompositeDataObject.Name;
            changeSet.ModifierName = UserUtil.GetCurrentUserName();
            changeSet.ChangeDatas = new List<TChangeData>();
            if (string.IsNullOrEmpty(changeSet.BillID))
            {
                throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_CreateDataChangeSet1,changeSet.CDOCode,changeLogContext.CurrentData == null));
            }
            return changeSet;
        }

        /// <summary>
        /// 递归获取新增Record的变更项
        /// </summary>
        /// <param name="cno"></param>
        /// <param name="newRecords"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        protected List<TChangeLog> GetCreateRecordChangeRecursive(CompositeNodeObject cno, List<TRecord> newRecords, int depth = 0)
        {
            List<TChangeLog> changeSets = new List<TChangeLog>();
            if (ChangeSetTemplate == null) return default;

            foreach (var record in newRecords)
            {
                var changeSet = GetCreateRecordChange(cno, record);
                //子节点ChangeData集合
                List<TChangeData> childChangeDatas = new List<TChangeData>();
                if (cno.ChildCompositeNodeObjects != null)
                {
                    foreach (var childCNO in cno.ChildCompositeNodeObjects)
                    {
                        var realationType = childCNO.GetRelationType();
                        TChangeData childChangeData = new TChangeData
                        {
                            ChangeDataType = realationType == 0 ? ChangeDataType.List : ChangeDataType.Record,
                            ColumnCode = TUtil.GetPropertyName(childCNO.DataObjectTableAlias, realationType == 0),
                            ColumnName = childCNO?.DataObject.Name,
                            SortOrder = 999,
                        };

                        List<TRecord> childRecords = (List<TRecord>)record[childCNO.GetCNODataObjectTableAlias()];
                        //如果包含下级，构造新的ChangeSet
                        if (childRecords.Count > 0)
                        {
                            var childSets = GetCreateRecordChangeRecursive(childCNO, childRecords, depth + 1);
                            if(childSets != null && childSets.Count > 0)
                            {
                                if (childChangeData.ChangeDataType == ChangeDataType.List)
                                {
                                    childChangeData.List = childSets;
                                }
                                else if(childChangeData.ChangeDataType == ChangeDataType.Record)
                                {
                                    childChangeData.Record = childSets.FirstOrDefault();
                                }

                                childChangeDatas.Add(childChangeData);
                            }
                        }
                    }
                }
                changeSet.ChangeDatas.AddRange(childChangeDatas);
                changeSets.Add(changeSet);
            }
            return changeSets;
        }

        /// <summary>
        /// 获取当前节点的TRecord的变更，要求TRecord的主键列必须赋值
        /// </summary>
        /// <param name="cno"></param>
        /// <param name="newRecord"></param>
        /// <returns>TChangeLog</returns>
        protected TChangeLog GetCreateRecordChange(CompositeNodeObject cno, TRecord newRecord)
        {
            TChangeLog changeSet = ChangeSetTemplate?.DACCloneChangeSet();
            changeSet.DataChangeType = TDataChangeType.New;
            if (changeSet == null)
            {
                throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_GetChangeSetTemplateFail));
                return default;
            }
            var normalEle = cno.Elements.Where(e => e.ElementType == ElementType.Normal).ToList();
            var changeDatas = new List<TChangeData>();
            string innerAssoEleName = null;
            var pkColName = cno.GetPKColumn().ColumnName;
            if (!cno.IsRootObject)
            {
                var innerAssoEle = cno.GetInnerAssociateElement();
                innerAssoEleName = innerAssoEle.Alias ?? innerAssoEle.Name;
            }

            foreach (var ele in normalEle)
            {
                var col = ele.GetDataColumn(cno);
                if (newRecord.IsContainProperty(col.ColumnName))
                {
                    object newVal = newRecord.GetValue(col.ColumnName);
                    var id = newRecord.GetValue(pkColName)?.ToString();
                    var sid = innerAssoEleName == null
                                        ? newRecord.GetValue(pkColName)?.ToString()
                                        : newRecord.GetValue(innerAssoEleName)?.ToString();
                    changeDatas.Add(new TChangeData
                    {
                        ChangeDataType = ChangeDataType.Normal,
                        ColumnCode = col.ColumnName,
                        ColumnName = col.DisplayName,
                        NewValue = newVal,    
                        OldValue = null,
                        SortOrder = col.ColumnOrder,
                    });
                }
            }
            changeSet.ChangeDatas = changeDatas;
            return changeSet;
        }

        /// <summary>
        /// 获取删除记录的变更项
        /// </summary>
        /// <param name="changeLogContext"></param>
        /// <returns></returns>
        protected List<TChangeLog> GetDeleteRecordChange(CompositeNodeObject cno, List<object> PKVal)
        {
            TChangeLog GetNewSet()
            {
                TChangeLog changeSet = ChangeSetTemplate?.DACCloneChangeSet();
                changeSet.DataChangeType = TDataChangeType.Delete;
                if (changeSet == null)
                {
                    throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_GetChangeSetTemplateFail));
                    return default;
                }
                return changeSet;
            }

            List<TChangeLog> changeSets = new List<TChangeLog>();

            if (PKVal == null)
                return null;
            List<TChangeData> changeDatas = new List<TChangeData>();
            var col = cno.GetPKColumn();
            foreach (var pk in PKVal)
            {
                var set = GetNewSet();
                var changeData = new TChangeData
                {
                    ChangeDataType = ChangeDataType.Normal,
                    ColumnCode = col.ColumnName,
                    ColumnName = col.DisplayName,
                    NewValue = pk,
                    OldValue = null,
                    SortOrder = col.ColumnOrder,
                };
                set.ChangeDatas.Add(changeData);
                changeSets.Add(set);
            }
            return changeSets;
        }

        /// <summary>
        /// 比较CNO节点下两个Record是否有变更
        /// </summary>
        /// <param name="cno">CNO节点元数据</param>
        /// <param name="newRecord">新Record</param>
        /// <param name="oldRecord">旧Record</param>
        /// <param name="changeDatas">返回的变更项</param>
        /// <returns>True:已变更,False:无变更</returns>
        protected bool Compare(CompositeNodeObject cno, TRecord newRecord, TRecord oldRecord, out List<TChangeData> changeDatas, int depth = 0)
        {
            var pkEle = cno.GetPKElement();
            var pkColName = cno.GetPKColumn().ColumnName;
            if (newRecord == null)
                throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_NewRecordIsNull));
            if (!FieldCompare(newRecord, oldRecord, pkEle))
                throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_RecordCompareFail_PkValueNotEqual));

            string innerAssoEleName = null;
            if (!cno.IsRootObject)
            {
                var innerAssoEle = cno.GetInnerAssociateElement();
                innerAssoEleName = innerAssoEle.Alias ?? innerAssoEle.Name;
            }

            changeDatas = new List<TChangeData>();
            var detailChangeDatas = new List<TChangeData>(); //明细变更项，只有主节点使用
            bool IsRecordChanged = false;
            var normalEle = cno.Elements.Where(e => e.ElementType == ElementType.Normal).ToList();
            foreach (var ele in normalEle)
            {
                var col = ele.GetDataColumn(cno);
                //处理系统字段
                //跳过时间戳字段比较
                if (col.IsTimestampField()) continue;

                if (newRecord.IsContainProperty(col.ColumnName))
                {
                    object oldVal = oldRecord.GetValueForDAC(col.ColumnName);
                    object newVal = newRecord.GetValueForDAC(col.ColumnName);
                    oldVal = PreprocessField(oldVal, ele.DataType);
                    newVal = PreprocessField(newVal, ele.DataType);
                    var flag = FieldCompare(oldVal, newVal, ele.DataType);
                    //判断字段是否变更，Record.Value为数据库取出来的类型，被DT做了类型转换，暂时先用默认的Equals方法
                    if (!flag)  //只记录变更项
                    {
                        IsRecordChanged = true;
                        detailChangeDatas.Add(new TChangeData
                        {
                            ChangeDataType = ChangeDataType.Normal,
                            ColumnCode = col.ColumnName,
                            ColumnName = col.DisplayName,
                            NewValue = newVal,
                            OldValue = oldVal,
                            SortOrder = col.ColumnOrder,
                        });
                    }
                }
            }
            changeDatas = detailChangeDatas;
            return IsRecordChanged;
        }

        /// <summary>
        /// 比较CNO节点下两个Record是否有变更，以newRecord中包含的字段为基准进行增量比较
        /// </summary>
        /// <param name="cno">CNO节点元数据</param>
        /// <param name="newRecord">新Record</param>
        /// <param name="oldRecord">旧Record</param>
        /// <param name="changeDatas">返回的变更项</param>
        /// <returns>True:已变更,False:无变更</returns>
        protected bool CompareAndSetChange(CompositeNodeObject cno, TRecord newRecord, TRecord oldRecord, ChangeLogControlContext controlContext, int depth = 0)
        {
            var pkEle = cno.GetPKElement();
            var pkColName = cno.GetPKColumn().ColumnName;
            var billID = newRecord.GetValue(pkColName);
            if (newRecord == null)
                throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_NewRecordIsNull));
            if (!FieldCompare(newRecord, oldRecord, pkEle))
                throw new Exception(string.Format(CommonStringClassEngine.AbstractChangeLog_RecordCompareFail_PkValueNotEqual));

            string innerAssoEleName = null;
            if (!cno.IsRootObject)
            {
                var innerAssoEle = cno.GetInnerAssociateElement();
                innerAssoEleName = innerAssoEle.Alias ?? innerAssoEle.Name;
            }

            var changeSetItems = new List<TChangeSetItem>(); //明细变更项，只有主节点使用
            bool isRecordChanged = false;
            var normalEle = cno.Elements.Where(e => e.ElementType == ElementType.Normal).ToList();
            foreach (var ele in normalEle)
            {
                DataColumn col = ele.GetDataColumn(cno);
                //处理系统字段
                //跳过时间戳字段比较
                if (col.IsTimestampField()) continue;
                //跳过创建人创建时间比较
                //???为什么没跳过最后修改信息的字段
                //因为要给变更日志记录用，需要ChangeSet里面有最后修改人 最后修改时间字段
                if (col.IsCreateInfoField())
                    continue;


                //Record默认包含字段值时，作为主列处理。要排除掩码列，因为掩码值如果不特殊请求，提交上来可能为掩码值，所以从掩码逻辑分支中处理
                if (newRecord.IsContainProperty(col.ColumnName) && !col.MaskingRule.IsEnable)
                {
                    object oldVal = oldRecord.GetValueForDAC(col.ColumnName);
                    object newVal = newRecord.GetValueForDAC(col.ColumnName);
                    //最后修改人信息，使用最新的Session中的值作比较
                    if (col.SysColumnFlag == SysColumnFlag.LastModifier || col.ColumnName == DataModelSystemConst.LastModifier)
                    {
                        newVal = BasicChangeInfo.Modifier;
                    }
                    if (col.SysColumnFlag == SysColumnFlag.LastModifierName || col.ColumnName == DataModelSystemConst.LastModifierName)
                    {
                        newVal = BasicChangeInfo.ModifierName;
                    }
                    oldVal = PreprocessField(oldVal, ele.DataType);
                    newVal = PreprocessField(newVal, ele.DataType);
                    var flag = FieldCompare(oldVal, newVal, ele.DataType);
                    //判断字段是否变更，Record.Value为数据库取出来的类型，被DT做了类型转换，暂时先用默认的Equals方法
                    if (!flag)  //只记录变更项
                    {
                        isRecordChanged = true;
                        changeSetItems.Add(new TChangeSetItem
                        {
                            ColumnCode = col.ColumnName,
                            ColumnName = col.DisplayName,
                            InitValue = oldVal,
                            InitDisplayName = null,
                            OldValue = oldVal,
                            OldDisplayValue = null, //外键字段如何取展示字段
                            NewValue = newVal,
                            NewDisplayValue = null,
                            SortOrder = col.ColumnOrder,
                            DataType = ele.DataType,
                        });
                    }
                }

                //国际化列只处理国际化i18N列
                if (col.IsInternational)
                {
                    Dictionary<string, string> mapping = InternationalTools.GetLanguageMapping(controlContext.InternationalMode, controlContext.SpecifiedLanguages);
                    if (mapping.Count > 0)
                    {
                        foreach (string i18NCode in mapping.Values)
                        {
                            string tRecordPropName = InternationalTools.GetLogicFieldName(col.ColumnName, i18NCode);

                            var item = ConstructUpdateChangeSetItem(ele, newRecord: newRecord, oldRecord: oldRecord, tRecordPropName, col.DisplayName + i18NCode, out bool isI18NChanged);
                            if (item != null)
                                changeSetItems.Add(item);
                            if (isI18NChanged)
                                isRecordChanged = true;
                        }
                    }
                }

                //掩码列处理：需同时处理主列+掩码列
                if (col.MaskingRule.IsEnable)
                {
                    if (TryGetMaskDataChangeItems(controlContext, cno, ele, newRecord, oldRecord, out List<TChangeSetItem> items))
                    {
                        isRecordChanged = true;
                        changeSetItems.AddRange(items);
                    }
                }
            }
            //是否发生record级别变更，如果是，初始化record的changeSet
            if (isRecordChanged)
            {
                if(newRecord.ChangeSet == null)
                {
                    newRecord.ChangeSet = new TChangeSet();
                }
                newRecord.ChangeSet.ChangeType = TRecordChangeType.Update;
                newRecord.ChangeSet.BillID = billID.ToString();
                newRecord.ChangeSet.CDOID = cno.CompositeDataObjectID;
                newRecord.ChangeSet.CDOCode = cno.CompositeDataObject?.Code;
                newRecord.ChangeSet.Modifier = BasicChangeInfo.Modifier;
                newRecord.ChangeSet.ModifierName = BasicChangeInfo.ModifierName;
                newRecord.ChangeSet.ModifyTime = BasicChangeInfo.ModifyTime;
                newRecord.ChangeSet.PKColumnName = pkColName;
                newRecord.ChangeSet.PKColumnValue = billID.ToString();
                newRecord.ChangeSet.TenantID = BasicChangeInfo.TenantID;
                newRecord.ChangeSet.AddRangeChangeData(changeSetItems);
            }
            return isRecordChanged;
        }
        #endregion

        #region 通用比较方法

        /// <summary>
        /// 比较TRecord两个值是否相等
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="ele"></param>
        /// <returns>true:相等 false:不相等</returns>
        protected bool FieldCompare(TRecord a, TRecord b, CompositeNodeObjectElement ele)
        {
            object v1 = a.GetValue(ele.DataColumn.ColumnName);
            object v2 = b.GetValue(ele.DataColumn.ColumnName);
            v1 = PreprocessField(v1);
            v2 = PreprocessField(v2);
            return FieldCompare(v1, v2, ele.DataType);
        }

        /// <summary>
        /// 比较TRecord两个值是否相等
        /// </summary>
        /// <param name="arg1"></param>
        /// <param name="arg2"></param>
        /// <param name="dataType"></param>
        /// <returns></returns>
        protected bool FieldCompare(object arg1, object arg2, DataType dataType)
        {
            arg1 = PreprocessField(arg1);
            arg2 = PreprocessField(arg2);

            if (dataType == DataType.Timestamp || dataType == DataType.DateTime || dataType == DataType.Date)
            {
                arg1 = ConvertToDateTime(arg1);
                arg2 = ConvertToDateTime(arg2);
            }
            if (dataType == DataType.Time)
            {
                arg1 = ConvertToTime(arg1);
                arg2 = ConvertToTime(arg2);
            }
            if (dataType == DataType.Boolean)
            {
                arg1 = ConvertToBoolean(arg1);
                arg2 = ConvertToBoolean(arg2);
            }

            return DataTypeToolFactory.GetCompare(dataType).EqualCompare(arg1, arg2);
        }

        /// <summary>
        /// 特殊处理日期类型
        /// </summary>
        /// <param name="timeObj"></param>
        /// <returns></returns>
        private static DateTime? ConvertToDateTime(object timeObj)
        {
            try
            {
                if (timeObj == null || string.IsNullOrEmpty(timeObj.ToString())) return null;
                if (timeObj is DateTime dateTime1) return dateTime1;
                else if (timeObj is Timestamp timestamp1)
                {
                    if (!string.IsNullOrEmpty(timestamp1.Data))
                        return Convert.ToDateTime(timestamp1.Data);
                }
                else if (timeObj is TRecord timestampRecord)
                {
                    var timeData = timestampRecord.GetValue("Data")?.ToString();
                    if (!string.IsNullOrEmpty(timeData))
                        return Convert.ToDateTime(timeData);
                }
                string str = timeObj.ToString();
                //前端传入的为JValue对象，取Value值
                if (timeObj is JValue timeJVal)
                {
                    str = timeJVal.Value?.ToString();
                }
                //前端传入的时间戳类型为JObject，特殊处理为{}的情况
                else if (timeObj is JObject timeJObj)
                {
                    if (timeObj.ToString() == "{}")
                        return null;
                    else
                    {
                        str = (timeJObj.GetValue(nameof(Timestamp.Data)) as JValue).Value.ToString();
                    }
                }
                if (DateTime.TryParse(str, out DateTime dateTime2))
                {
                    return dateTime2;
                }
                var timestamp = JsonConvert.DeserializeObject<Timestamp>(str);
                if (!string.IsNullOrEmpty(timestamp.Data))
                    return Convert.ToDateTime(timestamp.Data);
            }
            catch (Exception e)
            {
                s_localLogService.WriteError(e, $"转化日期或时间戳类型失败, 输入值:{timeObj?.ToString()}, 类型:{timeObj.GetType()}");
            }
            throw new FasException(DataAccessErrorCode.PlugIn.ChangeLog.Error, string.Format(CommonStringClassEngine.AbstractChangeLog_ConvertToDateTime,timeObj));
        }

        /// <summary>
        /// 特殊处理日期类型
        /// </summary>
        /// <param name="boolObj"></param>
        /// <returns></returns>
        private static bool? ConvertToBoolean(object boolObj)
        {
            try
            {
                if (boolObj == null || string.IsNullOrEmpty(boolObj.ToString())) return null;
                if (boolObj is bool boolVal) return boolVal;
                if (boolObj is int intVal)
                {
                    if (intVal == 0)
                        return false;
                    if (intVal == 1)
                        return true;
                }
                var strVal = boolObj?.ToString();
                if (bool.TryParse(strVal, out bool boolVal2))
                {
                    return boolVal2;
                }
                if (int.TryParse(strVal, out int intVal2))
                {
                    if (intVal2 == 0)
                        return false;
                    if (intVal2 == 1)
                        return true;
                }
            }
            catch (Exception e)
            {
                s_localLogService.WriteError(e, $"转化布尔类型失败, 输入值:{boolObj?.ToString()}, 类型:{boolObj.GetType()}");
            }
            throw new FasException(DataAccessErrorCode.PlugIn.ChangeLog.Error, string.Format(CommonStringClassEngine.AbstractChangeLog_ConvertToBoolean,boolObj));
        }

        /// <summary>
        /// 特殊处理日期类型
        /// </summary>
        /// <param name="boolObj"></param>
        /// <returns></returns>
        private static TimeSpan? ConvertToTime(object timeObj)
        {
            var result = TimeSpan.Zero;
            if (timeObj == null)
                return null;
            if(timeObj is TimeSpan ts)
                return ts;
            var timeStr = timeObj.ToString();
            if (string.IsNullOrEmpty(timeStr))
            {
                return null;
            }
            if (DateTime.TryParse(timeStr, out DateTime dt))
            {
                result = dt.TimeOfDay;
                return result;
            }
            else if (TimeSpan.TryParse(timeStr, out result))
            {
                return result;
            }
            return null;
        }

        /// <summary>
        /// 预处理待比较的对象(Action传入的)，如果为JValue取Value属性值，如果为DBNull返回Null
        /// </summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        protected object PreprocessField(object arg)
        {
            if (arg is JValue val)
            {
                return val.Value;
            }
            else if(arg is DBNull)
            {
                return null;
            }
            return arg;
        }

        /// <summary>
        /// 预处理待比较的对象(Action传入的)，如果为JValue取Value属性值，如果为DBNull返回Null
        /// </summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        protected object PreprocessField(object arg, DataType dataType)
        {
            arg = PreprocessField(arg);
            if (dataType == DataType.Timestamp || dataType == DataType.DateTime || dataType == DataType.Date)
            {
                arg = ConvertToDateTime(arg);
            }
            if (dataType == DataType.Time)
            {
                arg = ConvertToTime(arg);
            }
            if (dataType == DataType.Boolean)
            {
                arg = ConvertToBoolean(arg);
            }
            return arg;
        }
        #endregion

        #region 通用方法
        /// <summary>
        /// 通用序列化方法，特殊处理日期类型
        /// </summary>
        /// <param name="a"></param>
        /// <returns></returns>
        protected string JsonSerialize(object a)
        {
            if (a == null)
                return null;
            IsoDateTimeConverter timeConverter = new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd HH:mm:ss.fff" };
            return JsonConvert.SerializeObject(a, Formatting.Indented, timeConverter);
        }

        /// <summary>
        /// 调用运行时统一缓存服务获取状态机元数据集合
        /// </summary>
        /// <param name="cdo"></param>
        /// <returns></returns>
        protected List<StateMachineMetadata> InitCDOStateMachine(CompositeDataObject cdo)
        {
            List<StateMachineMetadata> result = new List<StateMachineMetadata>();
            var machines = cdo.RootCompositeNodeObject?.Elements.Where(e => e.StateMachines != null).SelectMany(e => e.StateMachines);
            var list = machines?.Where(m => m != null).ToList();
            try
            {
                if (list != null && list.Count > 0)
                {
                    foreach (var machine in list)
                    {
                        //调统一缓存服务运行时获取StateMachine 完整元数据
                        var realMachine = RTPMetadataHelper.GetRTPMetadata(machine.ID) as StateMachineMetadata;
                        result.Add(realMachine);
                    }
                }
                return result;
            }
            catch (Exception e)
            {

                throw new FasException(DataAccessErrorCode.PlugIn.ChangeLog.Error, string.Format(CommonStringClassEngine.AbstractChangeLog_InitCDOStateMachine,cdo.Code), e);
            }
        }

        /// <summary>
        /// 初始化Record的PkColumnName属性，方便后续Comparer比较器比较
        /// </summary>
        /// <param name="records"></param>
        /// <param name="cno"></param>
        protected void InitRecord(List<TRecord> records, CompositeNodeObject cno)
        {
            if (records != null)
            {
                foreach (var record in records)
                {
                    if (record != null)
                    {
                        record.PKColumnName = cno?.GetPKColumn()?.ColumnName;
                        record.TimestampColumnName = cno?.GetTimestampColumnForEngine()?.ColumnName;
                    }
                }
            }
        }

        /// <summary>
        /// 映射
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        protected TDataChangeType MappingInjectionTypeToChangeType(InjectionMethodType type)
        {
            switch (type)
            {
                case InjectionMethodType.Create:
                    return TDataChangeType.New;
                case InjectionMethodType.Update:
                    return TDataChangeType.Modify;
                case InjectionMethodType.Delete:
                    return TDataChangeType.Delete;
                case InjectionMethodType.Retrieve:
                default:
                    throw new Exception(CommonStringClassEngine.AbstractChangeLog_MappingInjectionTypeToChangeType);
            }
        }

        protected void InitBasicChangeInfo()
        {
            BasicChangeInfo = new TChangeBasicInfo
            {
                //ModifyTime = DateTime.Now,
                Modifier = UserUtil.GetCurrentUserID(),
                ModifierName = UserUtil.GetCurrentUserName(),
                TenantID = UserUtil.GetCurrentUserTenantId(),
            };
        }

        /// <summary>
        /// 初始化变更日志的控制上下文
        /// </summary>
        /// <param name="changeLogContext"></param>
        /// <returns></returns>
        protected ChangeLogControlContext InitChangeLogControlContext(ChangeLogContext changeLogContext)
        {
            Dictionary<string, object> controlContextDict = changeLogContext.ExtParam;
            ChangeLogControlContext controlContext = new ChangeLogControlContext();
            controlContext.InitAuthControl(changeLogContext.IsNeedAuth, changeLogContext.ColumnPermissionResult);
            if (controlContextDict != null)
            {
                if (controlContextDict.ContainsKey(DACConst.ExtParamSwitch.InternationalMode))
                {
                    controlContext.InternationalMode = (InternationalMode)controlContextDict[DACConst.ExtParamSwitch.InternationalMode];
                }
                if (controlContextDict.ContainsKey(DACConst.ExtParamSwitch.TargetLanguageList))
                {
                    controlContext.SpecifiedLanguages = controlContextDict[DACConst.ExtParamSwitch.TargetLanguageList] as List<string>;
                }
            }
            return controlContext;
        }
        #endregion

        private TChangeSetItem ConstructCrateChangeSetItem(CompositeNodeObjectElement ele, TRecord newRecord, string propName, string showName)
        {
            //新增时DBNull类型的不用记录
            object newVal = newRecord.GetValue(propName);
            if (newVal is DBNull || newVal == null || string.IsNullOrEmpty(newVal?.ToString())) return null;
            return new TChangeSetItem
            {
                ColumnCode = propName,
                ColumnName = showName,
                InitValue = null,
                InitDisplayName = null,
                OldValue = null,
                OldDisplayValue = null,
                NewValue = newVal,
                NewDisplayValue = null,
                SortOrder = ele.Sort,
                DataType = ele.DataType,
            };
        }

        private TChangeSetItem ConstructUpdateChangeSetItem(CompositeNodeObjectElement ele, string propName, string showName, object newVal, object oldVal)
        {
            return new TChangeSetItem
            {
                ColumnCode = propName,
                ColumnName = showName,
                InitValue = oldVal,
                InitDisplayName = null,
                OldValue = oldVal,
                OldDisplayValue = null, //外键字段如何取展示字段
                NewValue = newVal,
                NewDisplayValue = null,
                SortOrder = ele.Sort,
                DataType = ele.DataType,
            };
        }

        private TChangeSetItem ConstructUpdateChangeSetItem(CompositeNodeObjectElement ele, TRecord newRecord, TRecord oldRecord, string propName, string showName, out bool isChanged)
        {
            isChanged = false;
            //当前端传入的newRecord中包含该属性，则进行比较
            if (newRecord.IsContainProperty(propName))
            {
                object oldVal = oldRecord.GetValueForDAC(propName);
                object newVal = newRecord.GetValueForDAC(propName);
                oldVal = PreprocessField(oldVal, ele.DataType);
                newVal = PreprocessField(newVal, ele.DataType);
                var flag = FieldCompare(oldVal, newVal, ele.DataType);
                //判断字段是否变更，Record.Value为数据库取出来的类型，被DT做了类型转换，暂时先用默认的Equals方法
                if (!flag)  //只记录变更项
                {
                    isChanged = true;
                    var item = ConstructUpdateChangeSetItem(ele, propName, showName, newVal: newVal, oldVal: oldVal);
                    return item;
                }
            }
            return null;
        }

        /// <summary>
        /// 判断掩码列的主列数据是否变化，以Name列举例
        /// 1、当newRecord不存在Name_mask属性时，认为有更新？暂时认为没更新
        /// 2、当newRecord的Name_mask列为空时，与DB中oldRecord的Name_mask列比较
        ///     a.如果old.Name_mask不为空，则认为认为没更新
        ///     b.如果old.Name_mask为空，则认为为老数据，进行Name列变更比较
        ///         ①如果一致，则认为没有主列没有变更，返回
        ///         ②如果不一致，则认为有变更
        /// </summary>
        /// <param name="ele"></param>
        /// <param name="newRecord"></param>
        /// <param name="oldRecord"></param>
        /// <returns>false:认为没有更新 true:认为有更新</returns>
        private bool TryGetMaskDataChangeItems(ChangeLogControlContext controlContext, CompositeNodeObject cno, CompositeNodeObjectElement ele, TRecord newRecord, TRecord oldRecord, out List<TChangeSetItem> items)
        {
            items = new List<TChangeSetItem>();
            var columnName = ele.DataColumn.ColumnName;
            var maskColumnName = DataColumn.GetMaskColumnName(cno.DataObject, columnName);
            //当前端传入的newRecord中包含该属性，则进行比较
            //if (!newRecord.IsContainProperty(maskColumnName))
            //{
            //    return false;   //当newRecord不存在Name_mask属性时，暂时认为没更新  //2024-08-14 去掉该逻辑
            //}
            var newRecordMaskVal = PreprocessField(newRecord.GetValueForDAC(maskColumnName), DataType.String)?.ToString();
            //if (!string.IsNullOrEmpty(newRecordMaskVal))
            //{
            //    return false;   //当newRecord.Name_mask不为空时，认为没更新  //2024-08-14 去掉该逻辑
            //}

            var oldRecordMaskVal = PreprocessField(oldRecord.GetValueForDAC(maskColumnName), DataType.String)?.ToString();
            if (string.Equals(newRecordMaskVal, oldRecordMaskVal))
            {

            }

            object oldVal = oldRecord.GetValueForDAC(columnName);
            object newVal = newRecord.GetValueForDAC(columnName);
            oldVal = PreprocessField(oldVal, ele.DataType);
            newVal = PreprocessField(newVal, ele.DataType);
            bool flag = FieldCompare(oldVal, newVal, ele.DataType);
            //判断字段是否变更，Record.Value为数据库取出来的类型，被DT做了类型转换，暂时先用默认的Equals方法
            if (!flag)  //只记录变更项
            {
                //如果主列产生变更，则进行鉴权校验，是否具有改列的修改权限
                if (!controlContext.IsHasPermission(cno.DataObject, ele.DataColumn))
                {
                    return false;
                }
                TChangeSetItem mainFieldChangeItem = ConstructUpdateChangeSetItem(ele, columnName, ele.DataColumn.DisplayName, newVal: newVal, oldVal: oldVal);
                items.Add(mainFieldChangeItem);
                //掩码列的变更集生成放到外部
                return true;
            }
            return false;
        }

    }

    public class ChangeLogControlContext: AuthControlContext
    { 
        public InternationalMode InternationalMode { get; set; } = InternationalMode.All;

        public List<string> SpecifiedLanguages { get; set; }
    }
}
